"use strict";

var physics_printDebug = false;

function Simulator() {
	this.nodes = Array();
	this.elements = Array();
	
	this.connectNodeAndPort = function(node, port) {
		node.addPort(port);
		port.setNode(node);
	}
	
	this.connectNodeAndIndiePort = function(node, indiePort) {
		node.addIndiePort(indiePort);
		indiePort.setNode(node);
	}
	
	this.printNodes = function() {
		document.write("##### printNodes #######<br>");
		for(var n in this.nodes) {
			document.write("node " + n + "<br>");
			document.write("- - position " + this.nodes[n].position.elements + "<br>");
		}
	}
	
	this.printElements = function() {
		document.write("##### printElements #######<br>");
		for(var p in this.elements) {
			document.write("element " + p + "<br>");
			for(var o in this.elements) {
				document.write("- - port " + o + "<br>");
				document.write("- - - - - position " + this.elements[p].ports[o].node.position.elements + "<br>");
			}
		}
	}

	this.setZeroVelocity = function() {
		if (physics_printDebug) document.write("##### setZeroVelocity #######<br>");
		for(var n in this.nodes) {
			this.nodes[n].velocity = $V([0.0, 0.0]);
			this.nodes[n].force = $V([0.0, 0.0]);
			this.nodes[n].forceOld = $V([0.0, 0.0]);
		}
	}

	// could be static as well
	this.mergeSecondNodeToFirst = function(node0, node1) {
		for (var p in node1.indiePorts) {
			node0.addIndiePort(node1.indiePorts[p]);
			node1.indiePorts[p].setNode(node0);
		}
		
		// keep old gravity object iff new object also has a gravity object
		var nhasGravity = false;
		for (var p in node1.ports) {
			if (node1.ports[p].name == "gravity") {
				nhasGravity = true;
			} else {
				node0.addPort(node1.ports[p]);
				node1.ports[p].setNode(node0);
			}
		}
		
		if (!nhasGravity) {
			var newPorts = Array();
			for (var p in node0.ports) {
				if (node0.ports[p].name != "gravity") {
					newPorts.push(node0.ports[p]);
				}
			}
			node0.ports = newPorts;
		}
	}
	
	this.advanceTime = function(dt, damp) {
		if (physics_printDebug) document.write("##### advanceTime #######<br>");
		//console.log("next");
		for(var n in this.nodes) {
			var node = this.nodes[n];
			for (p in node.indiePorts) {
				var indiePort = node.indiePorts[p];
				if (indiePort.name == "ball") {
					for(var p in this.elements) {
						var element = this.elements[p];
						if (element.name == "rod") {
							// calculate distance to both line ends (d0 and d1)
							
							var d0 = node.position.subtract(element.ports[0].node.position);
							var d1 = node.position.subtract(element.ports[1].node.position);
							
							var dir = element.ports[1].node.position.subtract(element.ports[0].node.position);
							var dir_length = dir.modulus();
							var dir_proj = dir.dot(d0) / dir_length;
							//  nor = d0 -        dir             /dir_length * dir_proj
							var nor = d0.subtract(dir.multiply(1.0/dir_length * dir_proj));
							
							if (dir_proj > 0 && dir_proj < dir_length) {
								var v = nor;
								var v_length = nor.modulus();
							} else {
								if (dir_proj < 0) {
									var v = d0;
									var v_length = d0.modulus();
								} else {
									var v = d1;
									var v_length = d1.modulus();
								}
							}
							if (v_length < indiePort.radius) {
								/*console.log("----");
								console.log("dir_length " + dir_length);
								console.log("dir_proj " + dir_proj);
								console.log("d0 " + d0.elements);
								console.log("d1 " + d1.elements);
								console.log("nor " + nor.elements);
								console.log("v " + v.elements);*/
								
								var d = indiePort.radius - v_length;
								if (d*v_length < 1e-3) continue;
								//console.log(d);
								// (v/v_length)*d^2
								var force = v.multiply(1000.0/v_length * d*d);
								
								node.force = node.force.add(force);
								
								var dd0 = 1.0/d0.modulus();
								var dd1 = 1.0/d1.modulus();
								
								element.ports[0].node.force = element.ports[0].node.force.add(force.multiply(-dd0/(dd0 + dd1)));
								element.ports[1].node.force = element.ports[1].node.force.add(force.multiply(-dd1/(dd0 + dd1)));
							}
						}
					}
				}
			}
		}
		for(var n in this.nodes) {
			this.nodes[n].totalMass = 0.0;
			for (var p in this.nodes[n].ports) {
				//            mass +=                              ports[p].getMass();
				this.nodes[n].totalMass = this.nodes[n].totalMass + this.nodes[n].ports[p].getMass();
			}
			if (this.nodes[n].totalMass == 0.0) {
				alert("mass of node " + n + " is zero");
			}
		}
		for(var n in this.nodes) {
			for (var p in this.nodes[n].ports) {
				// force +=                                  ports[p].getForce(mass)
				this.nodes[n].force = this.nodes[n].force.add(this.nodes[n].ports[p].getForce(this.nodes[n].totalMass));
			}
		}
		for(var n in this.nodes) {
			//console.log(force.elements);
			// F = m * a
			// a = (p[-1] - 2*p[0] + p[1])/dt^2
			// => p[1] = F/m * dt^2 - p[-1] + 2*p[0]
			// => p[1] = p[0] + (p[0] - p[-1]) + F/m * dt^2
			// => p[1] = p[0] + damp*(p[0] - p[-1]) + F/m * dt^2
			if (physics_printDebug) console.log("force " + this.nodes[n].force.elements);
			if (physics_printDebug) console.log("vel " + this.nodes[n].velocity.elements);
			

			//var f = this.nodes[n].force.add(this.nodes[n].forceOld).multiply(0.5);
			var f = this.nodes[n].force;
			
			//       position =          position +   force             /mass * dt*dt -                   oldPosition +              position       * 2
			this.nodes[n].position =
				this.nodes[n].position
				.add(this.nodes[n].velocity.multiply(damp * dt))
				.add(this.nodes[n].force.multiply(1.0/this.nodes[n].totalMass * dt*dt));
			this.nodes[n].velocity = this.nodes[n].velocity.add(f.multiply(1.0/this.nodes[n].totalMass * dt));
			//this.nodes[n].position = this.nodes[n].position.add(this.nodes[n].force.multiply(dt*100.0));
		}
		for(var n in this.nodes) {
			this.nodes[n].forceOld = this.nodes[n].force;
			this.nodes[n].force = $V([0.0, 0.0]);
		}
	}
}

function SimulatableVar() {
	this.Node = function(position) {
		this.addPort = function(port) {
			this.ports.push(port);
		}
		this.addIndiePort = function(indiePort) {
			this.indiePorts.push(indiePort);
		}
		// variables
		this.velocity = null; // variable is initialized afterwards in the setZeroVelocity function
		this.position = position;
		this.totalMass = null;
		this.indiePorts = Array();
		this.ports = Array();
		this.force = $V([0.0, 0.0]);
		this.forceOld = $V([0.0, 0.0]);
	}
	
	this.Ball = function(radius) {
		this.setNode = function(node) {
			this.node = node;
		}
		this.radius = radius;
		this.name = "ball";
		this.node = null;
	}
	
	this.MassPoint = function(mass) {
		this.getForce = function(mass) {
			return $V([0.0,0.0]);
		}
		this.setNode = function(node) {
		}
		this.getMass = function() {
			return this.mass;
		}
		// variables
		this.mass = mass;
	}
	
	this.Gravity = function(gravity) {
		this.getForce = function(mass) {
			return gravity.multiply(mass);
		}
		this.setNode = function(node) {
		}
		this.getMass = function() {
			return 0.0;
		}
		// variables
		this.name = "gravity";
		this.grav = gravity;
	}
	
	// abstract class
	// function Element() {}
	
	this.Rod = function(length, radius, youngsModulus, density, internalFriction) {
		this.RodStressPort = function Rod_port(rod) {
			//this.node = NULL; set inside "setNode" function
			this.rod = rod;
			this.setNode = function(node) {
				this.node = node;
			}
			this.getForce = function(mass) {
				return this.getStiffnessForce().add(this.getFrictionForce(mass));
			}
			this.getMass = function() {
				var totalMass = this.rod.density * length * this.getArea();
				return totalMass / 2;
			}
			this.getArea = function() {
				return (radius*radius) * Math.PI;
			}
			// private
			this.getStiffnessForce = function() {
				if (this == this.rod.ports[0]) {
					var sign = -1;
				} else {
					var sign = 1;
				}
				if (physics_printDebug) document.write("pos0 " + rod.ports[0].node.position.elements + "<br>");
				if (physics_printDebug) document.write("pos1 " + rod.ports[1].node.position.elements + "<br>");
				
				var dir = rod.ports[0].node.position.subtract(rod.ports[1].node.position);
				var length_new = dir.modulus();
				var u = length_new - rod.length;
				//      dir            / length_new   *        u/         length   *            getArea()*youngsModulus*sign
				return (dir.multiply(1.0/length_new)).multiply(u/this.rod.length).multiply(this.getArea()*youngsModulus*sign);
			}
			this.getFrictionForce = function(mass) {
				if (this == this.rod.ports[0]) {
					var sign = -1;
				} else {
					var sign = 1;
				}
				var dir = rod.ports[0].node.position.subtract(rod.ports[1].node.position);
				var length_new = dir.modulus();
				// we have to divide the velocity by the rod length, just like we divide u by the rod length to get \epsilon
				var F = this.rod.ports[0].node.velocity.subtract(this.rod.ports[1].node.velocity).multiply(sign/this.rod.length*this.getMass()*this.rod.internalFriction);
				var F_par =  dir.multiply(F.dot(dir)/length_new);
				var F_nor = F.subtract(F_par);
				return F_nor.multiply(0.0).add(F_par);
			}
		}
		this.name = "rod";
		this.length = length;
		this.radius = radius;
		this.youngsModulus = youngsModulus;
		this.density = density;
		this.internalFriction = internalFriction;
		this.ports = new Array(new this.RodStressPort(this), new this.RodStressPort(this));
	}
}

var Simulatable = new SimulatableVar();
